<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="generator" content="VNote">

    <title>17_跨平台客户端</title>
    <link rel="icon" href="https://github.com/tamlok/vnote/raw/master/src/resources/icons/vnote.ico">

    <style type="text/css">
    /* STYLE_GLOBAL_PLACE_HOLDER */
    </style>

    <style type="text/css">
    *,
*::before,
*::after {
  box-sizing: border-box;
}

.container-fluid {
    width: 100%;
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;
}

.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
    position: relative;
    width: 100%;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
}

.col-12 {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100%;
    flex: 0 0 100%;
    max-width: 100%;
}

@media (min-width: 768px) {
    .col-md-3 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 25%;
        flex: 0 0 25%;
        max-width: 25%;
    }
}

@media (min-width: 768px) {
    .col-md-9 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 75%;
        flex: 0 0 75%;
        max-width: 75%;
    }
}

@media (min-width: 1200px) {
    .col-xl-2 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 16.666667%;
        flex: 0 0 16.666667%;
        max-width: 16.666667%;
    }
}

@media (min-width: 1200px) {
    .col-xl-10 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 83.333333%;
        flex: 0 0 83.333333%;
        max-width: 83.333333%;
    }
}

@media (min-width: 768px) {
    .pt-md-3, .py-md-3 {
        padding-top: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pb-md-3, .py-md-3 {
        padding-bottom: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pl-md-5, .px-md-5 {
        padding-left: 3rem!important;
    }
}

.d-none {
    display: none!important;
}

@media (min-width: 1200px) {
    .d-xl-block {
        display: block!important;
    }
}

@media (min-width: 768px) {
    .d-md-block {
        display: block!important;
    }
}

.bd-content {
    -webkit-box-ordinal-group: 1;
    -ms-flex-order: 0;
    order: 0;
}

.bd-toc {
    position: -webkit-sticky;
    position: sticky;
    top: 4rem;
    height: calc(100vh - 10rem);
    overflow-y: auto;
}

.bd-toc {
    -webkit-box-ordinal-group: 2;
    -ms-flex-order: 1;
    order: 1;
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
    font-size: .875rem;
}

.section-nav {
    padding-left: 0;
}

.section-nav ul {
    font-size: .875rem;
    list-style-type: none;
}

.section-nav li {
    font-size: .875rem;
}

.section-nav a {
    color: inherit !important;
}

.row {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
    margin-right: -15px;
    margin-left: -15px;
}

@media (min-width: 1200px) {
    .flex-xl-nowrap {
        flex-wrap: nowrap !important;
    }
}

#floating-button {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: #00897B;
    position: fixed;
    top: .5rem;
    right: .5rem;
    cursor: pointer;
    box-shadow: 0px 2px 5px #666;
}

#floating-button .more {
    color: #F5F5F5;
    position: absolute;
    top: 0;
    display: block;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
    padding: 0;
    margin: 0;
    line-height: 2.5rem;
    font-size: 2rem;
    font-family: 'monospace';
    font-weight: 300;
}

.hide-none {
    display: none !important;
}

.col-expand {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100% !important;
    flex: 0 0 100% !important;
    max-width: 100% !important;
    padding-right: 3rem !important;
}

.outline-bold {
    font-weight: bolder !important;
}

@media print {
    #floating-button {
        display: none !important;
    }
}

    @keyframes flash { 
  0% { color: rgb(128, 203, 196); }
  10% { color: rgb(0, 137, 123); }
  40% { color: rgb(0, 137, 123); }
  50% { color: rgb(128, 203, 196); }
  60% { color: rgb(0, 137, 123); }
  90% { color: rgb(0, 137, 123); }
}
.highlighted-anchor { animation: flash 1s; }
div.mark-rect { background: transparent; border: 5px solid rgb(87, 104, 196); border-radius: 2px; position: absolute; }
#vnote-footer { width: 100%; text-align: center; opacity: 0.2; margin-top: 3rem; }
#vnote-footer p { font-size: 0.8rem; }
#vnote-footer a { color: inherit !important; }
x-eqs { display: flex; flex-direction: row; align-content: space-between; align-items: center; }
x-eqs > x-eqn { width: 100%; margin-left: 3rem; }
x-eqs > span { text-align: right; }
.view-image, .view-svg { transition: 0.3s; }
.modal-box { display: none; position: fixed; z-index: 1000; padding-top: 50px; left: 0px; top: 0px; width: 100%; height: 100%; overflow: hidden; background-color: rgba(68, 68, 68, 0.952941); }
.modal-content { margin: auto; display: block; width: auto; height: auto; cursor: move; }
.modal-content { animation-name: zoom; animation-duration: 0.6s; }
@-webkit-keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
@keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
span.modal-close { position: absolute; z-index: 1000; top: 15px; right: 35px; color: rgb(218, 218, 218); font-size: 40px; font-weight: bold; transition: 0.3s; }
span.modal-close:hover, span.modal-close:focus { color: rgb(238, 238, 238); text-decoration: none; cursor: pointer; }
@media print {
  pre, pre code, td.hljs-ln-code { white-space: pre-wrap !important; word-break: break-all !important; }
  code, a { word-break: break-all !important; }
  div.flowchart-diagram, div.mermaid-diagram, div.plantuml-diagram { overflow: hidden !important; }
  img { max-width: 100% !important; height: auto !important; }
  #vnote-footer { display: none !important; }
}
.alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; }
.alert-primary { color: rgb(0, 64, 133); background-color: rgb(204, 229, 255); border-color: rgb(184, 218, 255); }
.alert-secondary { color: rgb(56, 61, 65); background-color: rgb(226, 227, 229); border-color: rgb(214, 216, 219); }
.alert-success { color: rgb(21, 87, 36); background-color: rgb(212, 237, 218); border-color: rgb(195, 230, 203); }
.alert-info { color: rgb(12, 84, 96); background-color: rgb(209, 236, 241); border-color: rgb(190, 229, 235); }
.alert-warning { color: rgb(133, 100, 4); background-color: rgb(255, 243, 205); border-color: rgb(255, 238, 186); }
.alert-danger { color: rgb(114, 28, 36); background-color: rgb(248, 215, 218); border-color: rgb(245, 198, 203); }
.alert-light { color: rgb(129, 129, 130); background-color: rgb(254, 254, 254); border-color: rgb(253, 253, 254); }
.alert-dark { color: rgb(27, 30, 33); background-color: rgb(214, 216, 217); border-color: rgb(198, 200, 202); }
.vnote-anchor { font-weight: 400; color: rgba(0, 123, 255, 0.498039); transition: color 0.16s linear; padding-left: 0.375em; -webkit-font-smoothing: antialiased; text-decoration: none; opacity: 0; }
.vnote-anchor:hover { color: rgb(0, 123, 255); text-decoration: none; opacity: 1; }
.vnote-anchor::after { content: attr(data-anchor-icon); }
.vnote-btn { position: relative; display: inline-block; padding: 6px 12px; font-size: 13px; font-weight: 700; line-height: 20px; white-space: nowrap; vertical-align: middle; cursor: pointer; border: none; user-select: none; -webkit-appearance: none; }
.vnote-copy-clipboard-btn { transition: opacity 0.3s ease-in-out; opacity: 0; padding: 2px 6px; position: absolute; top: 5px; right: 5px; }
pre:hover .vnote-copy-clipboard-btn { opacity: 1; }
pre.vnote-snippet { position: relative; }
body { margin: 0px auto; font-family: "Segoe UI", Helvetica, sans-serif, Tahoma, Arial, Geneva, Georgia, Palatino, "Times New Roman", "Hiragino Sans GB", 冬青黑体, "Microsoft YaHei", 微软雅黑, "Microsoft YaHei UI", "WenQuanYi Micro Hei", 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, "Liberation Sans", "Droid Sans", NSimSun, 新宋体, SimSun, 宋体; color: rgb(34, 34, 34); line-height: 1.5; padding: 15px; background: rgb(238, 238, 238); font-size: 16px; }
h1, h2, h3, h4, h5, h6 { color: rgb(34, 34, 34); font-weight: bold; margin-top: 20px; margin-bottom: 10px; padding: 0px; }
p { padding: 0px; margin-top: 16px; margin-bottom: 16px; }
h1 { font-size: 26px; }
h2 { font-size: 24px; }
h3 { font-size: 22px; }
h4 { font-size: 20px; }
h5 { font-size: 19px; }
h6 { font-size: 18px; }
a { color: rgb(0, 153, 255); margin: 0px; padding: 0px; vertical-align: baseline; text-decoration: none; word-break: break-word; }
a:hover { text-decoration: underline; color: rgb(255, 102, 0); }
a:visited { color: purple; }
ul, ol { padding: 0px 0px 0px 24px; }
li { line-height: 24px; }
li ul, li ol { margin-left: 16px; }
p, ul, ol { font-size: 16px; line-height: 24px; }
pre { display: block; overflow-y: hidden; overflow-x: auto; tab-size: 4; }
code { font-family: Consolas, Monaco, monospace, Courier; color: rgb(142, 36, 170); word-break: break-word; }
pre code { display: block; overflow-x: auto; padding: 0.5em; color: rgb(34, 34, 34); background-color: rgb(224, 224, 224); border-left: 0.5em solid rgb(0, 137, 123); line-height: 1.5; font-family: Consolas, Monaco, monospace, Courier; white-space: pre; tab-size: 4; }
pre code.markdown-metadata { border-left: 0.5em solid rgb(128, 203, 196); }
aside { display: block; float: right; width: 390px; }
blockquote { color: rgb(102, 102, 102); border-left: 0.5em solid rgb(122, 122, 122); padding: 0px 1em; margin-left: 0px; }
blockquote p { color: rgb(102, 102, 102); }
hr { display: block; text-align: left; margin: 1em 0px; border: none; height: 2px; background: rgb(153, 153, 153); }
table { padding: 0px; margin: 1rem 0.5rem; border-collapse: collapse; }
table tr { border-top: 2px solid rgb(204, 204, 204); background-color: white; margin: 0px; padding: 0px; }
table tr:nth-child(2n) { background-color: rgb(248, 248, 248); }
table tr th { font-weight: bold; border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr td { border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr th :first-child, table tr td :first-child { margin-top: 0px; }
table tr th :last-child, table tr td :last-child { margin-bottom: 0px; }
div.mermaid-diagram { margin: 16px 0px; overflow-y: hidden; }
div.flowchart-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.wavedrom-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.plantuml-diagram { padding: 5px 5px 0px; margin: 16px 0px; width: fit-content; overflow: hidden; }
.img-package { text-align: center; }
img.img-center { display: block; margin-left: auto; margin-right: auto; }
span.img-caption { min-width: 20%; max-width: 80%; display: inline-block; padding: 10px; margin: 0px auto; border-bottom: 1px solid rgb(192, 192, 192); color: rgb(108, 108, 108); text-align: center; line-height: 1.5; }
.emoji_zero, .emoji_one, .emoji_two, .emoji_three, .emoji_four, .emoji_five, .emoji_six, .emoji_seven, .emoji_eight, .emoji_nine { margin-left: 5px; margin-right: 8px; }
div.preview-hint { opacity: 0.5; margin-top: 30%; margin-bottom: 30%; align-items: center; display: flex; flex-direction: column; justify-content: center; }
table.hljs-ln tr { border: none; background-color: transparent; }
table.hljs-ln tr td { border: none; background-color: transparent; }
table.hljs-ln tr td.hljs-ln-numbers { user-select: none; text-align: center; color: rgb(170, 170, 170); border-right: 1px solid rgb(204, 204, 204); vertical-align: top; padding-right: 5px; white-space: nowrap; }
table.hljs-ln tr td.hljs-ln-code { padding-left: 10px; }
::-webkit-scrollbar { background-color: rgb(234, 234, 234); width: 14px; height: 14px; border: none; }
::-webkit-scrollbar-corner { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button { height: 14px; width: 14px; background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-button:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-track { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-thumb { border: none; background-color: rgb(218, 218, 218); }
::-webkit-scrollbar-thumb:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-thumb:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-button:horizontal:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(-90 256.00000000000006,256) " id="svg_1">   <polygon fill="%23333333" id="svg_2" points="128,192 256,320 384,192  "/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:horizontal:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(90 255.99999999999997,256.00000000000006) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="null" id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(180 255.99999999999997,256) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::selection { background: rgb(25, 118, 210); color: rgb(238, 238, 238); }
.modal-box { background-color: rgba(234, 234, 234, 0.952941); }
span.modal-close { color: rgb(102, 102, 102); }
span.modal-close:hover, span.modal-close:focus { color: rgb(34, 34, 34); }
.hljs { display: block; overflow-x: auto; padding: 0.5em; background: rgb(224, 224, 224); }
.hljs, .hljs-subst { color: rgb(54, 54, 54); }
.hljs-comment { color: rgb(118, 118, 118); }
.hljs-keyword, .hljs-attribute, .hljs-selector-tag, .hljs-meta-keyword, .hljs-doctag, .hljs-name { color: rgb(0, 0, 238); }
.hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: rgb(136, 0, 0); }
.hljs-title, .hljs-section { color: rgb(136, 0, 0); font-weight: bold; }
.hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: rgb(188, 96, 96); }
.hljs-literal { color: rgb(175, 0, 215); }
.hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { color: rgb(0, 135, 0); }
.hljs-meta { color: rgb(31, 113, 153); }
.hljs-meta-string { color: rgb(77, 153, 191); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: bold; }
.mermaid-diagram .mermaid .label { color: rgb(51, 51, 51); }
.mermaid-diagram .node rect, .mermaid-diagram .node circle, .mermaid-diagram .node ellipse, .mermaid-diagram .node polygon { fill: rgb(236, 236, 255); stroke: rgb(204, 204, 255); stroke-width: 1px; }
.mermaid-diagram .edgePath .path { stroke: rgb(51, 51, 51); }
.mermaid-diagram .edgeLabel { background-color: rgb(232, 232, 232); }
.mermaid-diagram .cluster rect { fill: rgb(255, 255, 222) !important; rx: 4 !important; stroke: rgb(170, 170, 51) !important; stroke-width: 1px !important; }
.mermaid-diagram .cluster text { fill: rgb(51, 51, 51); }
.mermaid-diagram .actor { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram text.actor { fill: black; stroke: none; }
.mermaid-diagram .actor-line { stroke: grey; }
.mermaid-diagram .messageLine0 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram .messageLine1 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram #arrowhead { fill: rgb(51, 51, 51); }
.mermaid-diagram #crosshead path { fill: rgb(51, 51, 51) !important; stroke: rgb(51, 51, 51) !important; }
.mermaid-diagram .messageText { fill: rgb(51, 51, 51); stroke: none; }
.mermaid-diagram .labelBox { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram .labelText { fill: black; stroke: none; }
.mermaid-diagram .loopText { fill: black; stroke: none; }
.mermaid-diagram .loopLine { stroke-width: 2; stroke: rgb(204, 204, 255); }
.mermaid-diagram .note { stroke: rgb(170, 170, 51); fill: rgb(255, 245, 173); }
.mermaid-diagram .noteText { fill: black; stroke: none; font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram .section { stroke: none; opacity: 0.2; }
.mermaid-diagram .section0 { fill: rgba(102, 102, 255, 0.490196); }
.mermaid-diagram .section2 { fill: rgb(255, 244, 0); }
.mermaid-diagram .section1, .mermaid-diagram .section3 { fill: white; opacity: 0.2; }
.mermaid-diagram .sectionTitle0 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle1 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle2 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle3 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle { text-anchor: start; font-size: 11px; }
.mermaid-diagram .grid .tick { stroke: lightgrey; opacity: 0.3; shape-rendering: crispEdges; }
.mermaid-diagram .grid path { stroke-width: 0; }
.mermaid-diagram .today { fill: none; stroke: red; stroke-width: 2px; }
.mermaid-diagram .task { stroke-width: 2; }
.mermaid-diagram .taskText { text-anchor: middle; font-size: 11px; }
.mermaid-diagram .taskTextOutsideRight { fill: black; text-anchor: start; font-size: 11px; }
.mermaid-diagram .taskTextOutsideLeft { fill: black; text-anchor: end; font-size: 11px; }
.mermaid-diagram .taskText0, .mermaid-diagram .taskText1, .mermaid-diagram .taskText2, .mermaid-diagram .taskText3 { fill: white; }
.mermaid-diagram .task0, .mermaid-diagram .task1, .mermaid-diagram .task2, .mermaid-diagram .task3 { fill: rgb(138, 144, 221); stroke: rgb(83, 79, 188); }
.mermaid-diagram .taskTextOutside0, .mermaid-diagram .taskTextOutside2 { fill: black; }
.mermaid-diagram .taskTextOutside1, .mermaid-diagram .taskTextOutside3 { fill: black; }
.mermaid-diagram .active0, .mermaid-diagram .active1, .mermaid-diagram .active2, .mermaid-diagram .active3 { fill: rgb(191, 199, 255); stroke: rgb(83, 79, 188); }
.mermaid-diagram .activeText0, .mermaid-diagram .activeText1, .mermaid-diagram .activeText2, .mermaid-diagram .activeText3 { fill: black !important; }
.mermaid-diagram .done0, .mermaid-diagram .done1, .mermaid-diagram .done2, .mermaid-diagram .done3 { stroke: grey; fill: lightgrey; stroke-width: 2; }
.mermaid-diagram .doneText0, .mermaid-diagram .doneText1, .mermaid-diagram .doneText2, .mermaid-diagram .doneText3 { fill: black !important; }
.mermaid-diagram .crit0, .mermaid-diagram .crit1, .mermaid-diagram .crit2, .mermaid-diagram .crit3 { stroke: rgb(255, 136, 136); fill: red; stroke-width: 2; }
.mermaid-diagram .activeCrit0, .mermaid-diagram .activeCrit1, .mermaid-diagram .activeCrit2, .mermaid-diagram .activeCrit3 { stroke: rgb(255, 136, 136); fill: rgb(191, 199, 255); stroke-width: 2; }
.mermaid-diagram .doneCrit0, .mermaid-diagram .doneCrit1, .mermaid-diagram .doneCrit2, .mermaid-diagram .doneCrit3 { stroke: rgb(255, 136, 136); fill: lightgrey; stroke-width: 2; cursor: pointer; shape-rendering: crispEdges; }
.mermaid-diagram .doneCritText0, .mermaid-diagram .doneCritText1, .mermaid-diagram .doneCritText2, .mermaid-diagram .doneCritText3 { fill: black !important; }
.mermaid-diagram .activeCritText0, .mermaid-diagram .activeCritText1, .mermaid-diagram .activeCritText2, .mermaid-diagram .activeCritText3 { fill: black !important; }
.mermaid-diagram .titleText { text-anchor: middle; font-size: 18px; fill: black; }
.mermaid-diagram .node text { font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram div.mermaidTooltip { position: absolute; text-align: center; max-width: 200px; padding: 2px; font-family: "trebuchet ms", verdana, arial; font-size: 12px; background: rgb(255, 255, 222); border: 1px solid rgb(170, 170, 51); border-radius: 2px; pointer-events: none; z-index: 100; }

    </style>

    <script type="text/javascript">
var toc = [];

var setVisible = function(node, visible) {
    var cl = 'hide-none';
    if (visible) {
        node.classList.remove(cl);
    } else {
        node.classList.add(cl);
    }
};

var isVisible = function(node) {
    var cl = 'hide-none';
    return !node.classList.contains(cl);
};

var setPostContentExpanded = function(node, expanded) {
    var cl = 'col-expand';
    if (expanded) {
        node.classList.add(cl);
    } else {
        node.classList.remove(cl);
    }
};

var setOutlinePanelVisible = function(visible) {
    var outlinePanel = document.getElementById('outline-panel');
    var postContent = document.getElementById('post-content');

    setVisible(outlinePanel, visible);
    setPostContentExpanded(postContent, !visible);
};

var isOutlinePanelVisible = function() {
    var outlinePanel = document.getElementById('outline-panel');
    return isVisible(outlinePanel);
};

window.addEventListener('load', function() {
    var outlinePanel = document.getElementById('outline-panel');
    outlinePanel.style.display = 'initial';

    var floatingContainer = document.getElementById('container-floating');
    floatingContainer.style.display = 'initial';

    var outlineContent = document.getElementById('outline-content');
    var postContent = document.getElementById('post-content');

    // Escape @text to Html.
    var escapeHtml = function(text) {
        var map = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;'
        };

        return text.replace(/[&<>"']/g, function(m) { return map[m]; });
    }

    // Fetch the outline.
    var headers = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");
    toc = [];
    for (var i = 0; i < headers.length; ++i) {
        var header = headers[i];

        toc.push({
            level: parseInt(header.tagName.substr(1)),
            anchor: header.id,
            title: escapeHtml(header.textContent)
        });
    }

    if (toc.length == 0) {
        setOutlinePanelVisible(false);
        setVisible(floatingContainer, false);
        return;
    }

    var baseLevel = baseLevelOfToc(toc);
    var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel);

    outlineContent.innerHTML = tocTree;
    setOutlinePanelVisible(true);
    setVisible(floatingContainer, true);
});

// Return the topest level of @toc, starting from 1.
var baseLevelOfToc = function(p_toc) {
    var level = -1;
    for (i in p_toc) {
        if (level == -1) {
            level = p_toc[i].level;
        } else if (level > p_toc[i].level) {
            level = p_toc[i].level;
        }
    }

    if (level == -1) {
        level = 1;
    }

    return level;
};

// Handle wrong title levels, such as '#' followed by '###'
var toPerfectToc = function(p_toc, p_baseLevel) {
    var i;
    var curLevel = p_baseLevel - 1;
    var perfToc = [];
    for (i in p_toc) {
        var item = p_toc[i];

        // Insert empty header.
        while (item.level > curLevel + 1) {
            curLevel += 1;
            var tmp = { level: curLevel,
                        anchor: '',
                        title: '[EMPTY]'
                      };
            perfToc.push(tmp);
        }

        perfToc.push(item);
        curLevel = item.level;
    }

    return perfToc;
};

var itemToHtml = function(item) {
    return '<a href="#' + item.anchor + '" data="' + item.anchor + '">' + item.title + '</a>';
};

// Turn a perfect toc to a tree using <ul>
var tocToTree = function(p_toc, p_baseLevel) {
    var i;
    var front = '<li>';
    var ending = ['</li>'];
    var curLevel = p_baseLevel;
    for (i in p_toc) {
        var item = p_toc[i];
        if (item.level == curLevel) {
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        } else if (item.level > curLevel) {
            // assert(item.level - curLevel == 1)
            front += '<ul>';
            ending.push('</ul>');
            front += '<li>';
            front += itemToHtml(item);
            ending.push('</li>');
            curLevel = item.level;
        } else {
            while (item.level < curLevel) {
                var ele = ending.pop();
                front += ele;
                if (ele == '</ul>') {
                    curLevel--;
                }
            }
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        }
    }
    while (ending.length > 0) {
        front += ending.pop();
    }
    front = front.replace("<li></li>", "");
    front = '<ul>' + front + '</ul>';
    return front;
};

var toggleMore = function() {
    if (toc.length == 0) {
        return;
    }

    var p = document.getElementById('floating-more');
    if (isOutlinePanelVisible()) {
        p.textContent = '<';
        setOutlinePanelVisible(false);
    } else {
        p.textContent = '>';
        setOutlinePanelVisible(true);
    }
};

window.addEventListener('scroll', function() {
    if (toc.length == 0 || !isOutlinePanelVisible()) {
        return;
    }

    var postContent = document.getElementById('post-content');
    var scrollTop = document.documentElement.scrollTop
                    || document.body.scrollTop
                    || window.pageYOffset;
    var eles = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");

    if (eles.length == 0) {
        return;
    }

    var idx = -1;
    var biaScrollTop = scrollTop + 50;
    for (var i = 0; i < eles.length; ++i) {
        if (biaScrollTop >= eles[i].offsetTop) {
            idx = i;
        } else {
            break;
        }
    }

    var header = '';
    if (idx != -1) {
        header = eles[idx].id;
    }

    highlightItemOnlyInOutline(header);
});

var highlightItemOnlyInOutline = function(id) {
    var cl = 'outline-bold';
    var outlineContent = document.getElementById('outline-content');
    var eles = outlineContent.querySelectorAll("a");
    var target = null;
    for (var i = 0; i < eles.length; ++i) {
        var ele = eles[i];
        if (ele.getAttribute('data') == id) {
            target = ele;
            ele.classList.add(cl);
        } else {
            ele.classList.remove(cl);
        }
    }

    // TODO: scroll target into view within the outline panel scroll area.
};

</script>


<!-- HEAD_PLACE_HOLDER -->
</head>
<body>
<div class="container-fluid">
<div class="row flex-xl-nowrap">
    <div id="outline-panel" style="display:none;" class="d-none d-md-block d-xl-block col-md-3 col-xl-2 bd-toc">
        <div id="outline-content" class="section-nav"></div>
    </div>
    <div id="post-content" class="col-12 col-md-9 col-xl-10 py-md-3 pl-md-5 bd-content">
    <div style="page-break-after: always;"></div>
<h1 id="toc_0">17. 跨平台客户端<a class="vnote-anchor" href="#toc_0" data-anchor-icon="#"></a></h1>
<p><img src='' alt="" class="view-image"></p>
<p>  当前版本的框架主要单元仅针对Delphi的Win32/Win64系统，以及（初步状态下）FPC下的Windows或Linux。</p>
<p>  它允许为公司本地业务应用程序轻松创建自托管mORMot服务器，或廉价的云托管，因为mORMot CPU和RAM要求远低于通常的<code>IIS-WCF-MSSQL-.Net</code>堆栈。</p>
<p>  但在<a href="">面向服务的架构（SOA）</a>中，您可能需要为平台支持集之外的平台创建客户端，尤其是移动设备或AJAX应用程序。</p>
<p>  因此，源代码存储库的<code>CrossPlatform</code>子文件夹中提供了一组跨平台客户端单元，它允许用现代pascal对象语言编写任何客户端：</p>
<ul>
<li>任何版本的Delphi，在任何平台上（Mac OSX，或各种移动设备）；</li>
<li>FreePascal编译器（在2.6.4、2.7.1或3.1.1分支）；</li>
<li>Smart Mobile Studio（2.1及更高版本），用于创建AJAX或移动应用程序（如果需要，可通过PhoneGap）。</li>
</ul>
<p>  单元如下：</p>
<table>
<thead>
<tr>
<th>单元名称</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>SynCrossPlatformREST.pas</td>
<td>主要单元，实现安全的ORM和SOA RESTful客户端</td>
</tr>
<tr>
<td>SynCrossPlatformCrypto.pas</td>
<td>SHA256和crc32算法，用于身份验证</td>
</tr>
<tr>
<td>SynCrossPlatformJSON.pas</td>
<td>优化的JSON流程（Smart不使用）</td>
</tr>
<tr>
<td>SynCrossPlatformSpecific.pas</td>
<td>系统特定的功能，例如 HTTP客户端</td>
</tr>
</tbody>
</table>
<p>  这套单元将为任何类型的客户端提供坚实的共享基础：</p>
<ul>
<li>通过HTTP连接到mORMot服务器，具有完整的REST支持；</li>
<li>支持弱或默认身份验证以确保传输安全；</li>
<li><code>TSQLRecord</code>类定义，在Delphi或FreePascal上使用RTTI，并为Smart Mobile Studio生成代码；</li>
<li>远程CRUD操作，通过JSON和REST，使用<code>TSQLRestClientURI</code>类，使用与<code>mORMot.pas</code>框架单元相同的方法；</li>
<li>优化的<code>TSQLTableJSON</code>类，用于处理由mORMot的REST服务器ORM返回的JSON结果表 - 请参阅<a href="">JSON（非）扩展布局</a>；</li>
<li>批处理，参见用于<a href="">添加/更新/删除记录的批次序列</a>，用于事务和高速写入；</li>
<li>带参数编码的<a href="">基于方法的客户端-服务端服务</a>；</li>
<li>带参数编码和实例生命周期的<a href="">基于接口的客户端-服务端服务</a>；</li>
<li>支持大多数字段类型的映射，包括如ISO 8601日期/时间编码，BLOB和<code>TModTime</code>/<code>TCreateTime</code>，参见<a href="">TSQLRecord字段定义</a>；</li>
<li>复杂<code>record</code>类型也可以通过JSON在所有平台上导出和使用（ORM和SOA方法）；</li>
<li>ORM和SOA进程使用的集成调试方法，日志记录到本地文件或远程服务器；</li>
<li>一些跨平台的底层函数和类型定义，以帮助在项目之间尽可能的共享代码。</li>
</ul>
<p>  将来，可也许支持编写C＃或Java客户端。</p>
<p>  <code>CrossPlatform</code>子文件夹代码可用作参考，以在任何平台上编写最小和最有效的客户端。我们的REST模型非常简单和标准，使用JSON往往会避免XML或二进制格式可能出现的许多潜在的编码问题。</p>
<p>  实际上，嵌入在mORMot服务端的代码生成器可用于创建客户端包装器，使用服务端包含的<a href="">Mustache模版引擎</a>内容。只需点击，您就可以为任何支持的平台生成和下载客户端源文件。提供了一组<code>.mustache</code>模板，可以自定义或扩展以支持任何新平台：欢迎任何帮助，特别是针对Java或C＃客户端。</p>
<h2 id="toc_1">17.1. 支持的客户端平台<a class="vnote-anchor" href="#toc_1" data-anchor-icon="#"></a></h2>
<h3 id="toc_2">17.1.1. Delphi FMX/FreePascal FCL跨平台支持<a class="vnote-anchor" href="#toc_2" data-anchor-icon="#"></a></h3>
<p>  最新版本的Delphi包括FireMonkey（FMX）框架，能够为Windows、Mac OSX、Android和iOS（iPhone/iPad）提供多设备交付、真正的原生应用程序。</p>
<p>  我们的<code>SynCrossPlatform*</code>单元可以轻松地为这些平台创建客户端。</p>
<p>  类似地，可以使用FreePascal编译这些单元，以便通过该编译器支持的众多平台使用各种mORMot服务。</p>
<p>  要使用这些单元，请在IDE中确保在库搜索路径中定义了mORMot源代码存储库的<code>CrossPlatform</code>子文件夹。</p>
<h4 id="toc_3">17.1.1.1. 跨平台JSON<a class="vnote-anchor" href="#toc_3" data-anchor-icon="#"></a></h4>
<p>  我们在<code>SynCrossPlatformJSON.pas</code>中开发了自己的跨平台JSON处理单元，与Delphi和FreePascal共享。</p>
<p>  实际上，它看来更容易使用（因为它是基于变体的，具有后期绑定能力）并且比最新版本的Delphi附带的官方<code>DBXJSON.pas</code>单元运行快得多，如<code>25 - JSON performance</code>示例所述。</p>
<pre><code class="lang-console hljs"> 2.2. Table content:
- Synopse crossplatform: 41,135 assertions passed  20.56ms  400,048/s  1.9 MB
- DBXJSON: 41,136 assertions passed  240.84ms  34,159/s  9.9 MB
</code></pre>
<p>  在处理mORMot服务端返回的结果列表时，我们的<code>TSQLTableJSON</code>类比标准<code>DBXJSON</code>单元快10倍以上。</p>
<p>  上面每行的最后一位值是内存消耗。 它应该是更有利于移动平台，因其内存分配往往比Windows（FastMM4内存管理器确实很神奇）更慢、更敏感。 我们的单元比RTL版本消耗的内存少5倍。</p>
<p>  我们这里没有包含<code>XSuperObject</code>，这也是跨平台的，但在速度方面表现甚至比<code>DBXJSON</code>更差。 其他库，如<code>SuperObject</code>或<code>dwsJSON</code>，不是跨平台的。</p>
<p>  有关此比较的详细信息，请参见http://blog.synopse.info/post/json-benchmark-delphi-mormot-superobject-dwsjson-dbxjson。</p>
<p>  特别值得一提的是<code>dwsJSON</code>，它表现得非常好，但仅限于Windows，并且比mORMot的实现要慢：</p>
<pre><code class="lang-console hljs">- Synopse ORM loop: 41,135 assertions passed  6.18ms  1,330,153/s  1.1 MB
- Synopse ORM list: 41,135 assertions passed  6.47ms  1,270,775/s  952 KB
- Synopse crossplatform: 41,135 assertions passed  20.56ms  400,048/s  1.9 MB
- Super object properties: 41,136 assertions passed  2.20s  3,739/s  6.3 MB
- dwsJSON: 41,136 assertions passed  32.05ms  256,628/s  4.7 MB
- DBXJSON: 41,136 assertions passed  240.84ms  34,159/s  9.9 MB
</code></pre>
<p>  <code>"Synopse ORM"</code>行代表<code>mORMot.pas</code>中实现的<code>TSQLTableJSON</code>类。 它使用我们优化的UTF-8函数和类，与<code>SynCommons.pas</code>中实现的<code>RawUTF8</code>自定义字符串类型一起直接就地转义，因此它比我们的跨平台单元快3倍，比<code>DBXJSON</code>快40倍、更少的内存消耗。 <code>Synopse ORM</code>使用的一些技巧依赖于指针并且与NextGen编译器或Delphi官方路线图不兼容，因此Synopse跨平台使用不同的算法，但仍提供相当不错的性能。</p>
<p>  此单元拥有<code>TJSONVariantData</code>自定义变体类型，类似于<a href="">TDocVariant自定义变体类型</a>，可在mORMot框架中使用。</p>
<p>  它允许编写具有良好可读性的整洁的代码，并具有后期绑定：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> doc: variant;
    json,json2: <span class="hljs-keyword">string</span>;
...
  doc := JSONVariant(<span class="hljs-string">'{"test":1234,"name":"Joh\\"n\\r","zero":0.0}'</span>);
  assert(doc.test=<span class="hljs-number">1234</span>);
  assert(doc.<span class="hljs-keyword">name</span>=<span class="hljs-string">'Joh"n'</span><span class="hljs-string">#13</span>);
  assert(doc.name2=null);
  assert(doc.zero=<span class="hljs-number">0</span>);
  json := doc; <span class="hljs-comment">// conversion to JSON text when assigned to a string variable</span>
  assert(json=<span class="hljs-string">'{"test":1234,"name":"Joh\\"n\\r","zero":0}'</span>);
  doc.name2 := <span class="hljs-number">3.1415926</span>;
  doc.<span class="hljs-keyword">name</span> := <span class="hljs-string">'John'</span>;
  json := doc;
  assert(json=<span class="hljs-string">'{"test":1234,"name":"John","zero":0,"name2":3.1415926}'</span>);
</code></pre>
<p>  该单元还能够序列化任何<code>TPersistent</code>类，即可以从JSON对象表示中读写所有发布属性。 它还能处理嵌套对象，存储为TCollection。</p>
<p>  参见<code>SynCrossPlatformTests</code>单元：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TMainNested</span> = <span class="hljs-keyword">class</span>(TCollectionItem)
  <span class="hljs-keyword">private</span>
    fNumber: double;
    fIdent: RawUTF8;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> Ident: RawUTF8 <span class="hljs-keyword">read</span> fIdent <span class="hljs-keyword">write</span> fIdent;
    <span class="hljs-keyword">property</span> Number: double <span class="hljs-keyword">read</span> fNumber <span class="hljs-keyword">write</span> fNumber;
  <span class="hljs-keyword">end</span>;

  <span class="hljs-title">TMain</span> = <span class="hljs-keyword">class</span>(TPersistent)
  <span class="hljs-keyword">private</span>
    fName: RawUTF8;
    fNested: TCollection;
    fList: TStringList;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">destructor</span> <span class="hljs-title">Destroy</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> <span class="hljs-keyword">Name</span>: RawUTF8 <span class="hljs-keyword">read</span> fName <span class="hljs-keyword">write</span> fName;
    <span class="hljs-keyword">property</span> Nested: TCollection <span class="hljs-keyword">read</span> fNested;
    <span class="hljs-keyword">property</span> List: TStringList <span class="hljs-keyword">read</span> fList;
  <span class="hljs-keyword">end</span>;

  obj1 := TMain.Create;
  obj2 := TMain.Create;
...
  obj1.<span class="hljs-keyword">Name</span> := IntToStr(i);
  item := obj1.Nested.Add <span class="hljs-keyword">as</span> TMainNested;
  item.Ident := obj1.<span class="hljs-keyword">Name</span>;
  item.Number := i/<span class="hljs-number">2</span>;
  obj1.list.Add(obj1.<span class="hljs-keyword">Name</span>);
  json := ObjectToJSON(obj1);
  <span class="hljs-keyword">if</span> i=<span class="hljs-number">1</span> <span class="hljs-keyword">then</span>
    assert(json=<span class="hljs-string">'{"Name":"1","Nested":[{"Ident":"1","Number":0.5}],"List":["1"]}'</span>);
  JSONToObject(obj2,json);
  assert(obj2.Nested.Count=i);
  json2 := ObjectToJSON(obj2);
  assert(json2=json);
...
</code></pre>
<p>  当然，此序列化功能用于<code>TSQLRecord</code> ORM类。</p>
<p>  由于RTTI的不足，服务端使用代码包装器生成的某些函数支持<code>record</code>序列化。</p>
<h4 id="toc_4">17.1.1.2. Delphi OSX和NextGen<a class="vnote-anchor" href="#toc_4" data-anchor-icon="#"></a></h4>
<p>  为了符合NextGen版本，我们的<code>SynCrossPlatform*</code>单元遵循这个针对Android和iOS的新系列交叉编译器的要求。</p>
<p>  特别是，我们只依赖字符串类型进行文本处理和存储，即使在JSON级别，我们也尝试使对象分配与ARC兼容。定义了一些类型，如<code>THttpBody</code>、<code>TUTF8Buffer</code>或<code>AnsiChar</code>，以确保我们的单元能在所有支持的平台上编译。</p>
<p>  在Delphi上，Indy库用于HTTP请求。它本质上是跨平台的，因此应该在任何支持的系统上工作。有关iOS和Android客户端的SSL支持，请按照http://blog.marcocantu.com/blog/using_ssl_delphi_ios.html上的说明进行操作，您也可以从http://indy.fulgan.com/SSL/OpenSSLStaticLibs.7z下载所需的<code>libcrypto.a</code>和<code>libssl.a</code>文件。</p>
<p>  移动设备需要通过FMX反馈。</p>
<p>  事实上，我们依靠Smart Mobile Studio项目来支持我们的移动应用程序，因此Synopse团队没有像其他系统一样深入测试Delphi NextGen平台（即iOS和Android）。您的意见非常宝贵，欢迎！</p>
<h4 id="toc_5">17.1.1.3. FreePascal客户端<a class="vnote-anchor" href="#toc_5" data-anchor-icon="#"></a></h4>
<p>  <code>SynCrossPlatform*</code>单元在其2.7.1/3.1.1分支中支持FreePascal编译器。</p>
<p>  大多数代码都与Delphi共享，包括RTTI支持和所有支持的类型。</p>
<p>  但是有一些限制。</p>
<p>  由于FreePascal变体实现后期绑定的错误，以下代码将无法按预期工作：</p>
<pre><code class="lang-pascal hljs">  doc.name2 := <span class="hljs-number">3.1415926</span>;
  doc.<span class="hljs-keyword">name</span> := <span class="hljs-string">'John'</span>;
</code></pre>
<p>  在FreePascal下，你必须写：</p>
<pre><code class="lang-pascal hljs">  TJSONVariantData(doc)[<span class="hljs-string">'name2'</span>] := <span class="hljs-number">3.1415926</span>;
  TJSONVariantData(doc)[<span class="hljs-string">'name'</span>] := <span class="hljs-string">'John'</span>;
</code></pre>
<p>  事实上，FreePascal RTL后期绑定属性的实现方式禁止修改相关变体的内容。创建变体的私有副本，不仅速度较慢，而且不允许修改其存储值。</p>
<p>  欢迎来自FreePascal维护人员的任何反馈和帮助！</p>
<p>  因此，在使用该编译器时，直接访问<code>TJSONVariantData</code>实例而不是变体变量将更快且更不容易出错，直到问题得到解决。</p>
<p>  在Lazarus IDE中，我们还发现调试器无法处理我们的自定义变体类型。如果您使用调试器查看任何<code>TJSONVariantData</code>实例，将显示错误消息<code>"unsupported variant type"</code>。据我们所知，这是一个Lazarus bug。而Delphi，在转换为字符串（即JSON表示）之后，能够在调试器中显示任何定制的变体类型。</p>
<p>  2.7.1/3.1.1修订版的另一个问题是如何实现新的字符串类型。</p>
<p>  实际上，如果您使用包含UTF-8文本编码的字符串变量，则以下行将结果的代码页重置为系统代码页：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">StringToJSON</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Text: <span class="hljs-keyword">string</span>)</span>:</span> <span class="hljs-keyword">string</span>;
  ...
  result := <span class="hljs-string">'"'</span>+copy(Text,<span class="hljs-number">1</span>,j-<span class="hljs-number">1</span>); <span class="hljs-comment">// here FPC 2.7.1 erases UTF-8 encoding</span>
  ...
</code></pre>
<p>  听看来好像<code>'''</code>会强制结果的代码页不使用UTF-8内容。</p>
<p>  使用Delphi，这种语句按预期工作，即使对于<code>AnsiString</code>值，并且<code>'"'</code>常量也作为<code>RawByteString</code>处理。我们无法为FPC找到一个简单而安全的解决方法。欢迎专家在这方面提出任何意见！</p>
<p>  如果您使用FPC（和Lazarus）用于Windows操作系统，则必须小心处理此限制。在其他系统中，默认代码页可能是UTF-8，因此在这种情况下，我们的<code>SynCrossPlatform*</code>单元将按预期工作。</p>
<p>  我们发现FreePascal编译器工作得很好，生成的可执行文件小而快。对于大多数常见的工作，效率与Delphi相当。经过简单粗略的线程测试，内存管理器不如FastMM4优化，但它是跨平台的，并且设计为多线程模式时更高效：事实上，它没有巨大的锁定，而FastMM4存在。</p>
<h4 id="toc_6">17.1.1.4. 本地或远程日志记录<a class="vnote-anchor" href="#toc_6" data-anchor-icon="#"></a></h4>
<p>  您可以使用<code>TSQLRest.Log()</code>重载方法将任何日志内容记录到文件或远程服务器中。</p>
<p>  <code>TSQLRest</code>实例的所有ORM和SOA函数都会创建需要的日志，只需使用mORMot单元并在Win32/Win64上运行。</p>
<p>  如以下是在<code>RegressionTest.dpr</code>过程创建的一些日志条目：</p>
<pre><code class="lang-console hljs">16:47:15 Trace  POST root/People status=201 state=847 in=92 out=0
16:47:15 DB  People.ID=200 created from {"FirstName":"First200","LastName":"Last200","YearOfBirth":2000,"YearOfDeath":2025,"Sexe":0}
16:47:15 SQL  select RowID,FirstName,LastName,YearOfBirth,YearOfDeath,Sexe from People
16:47:15 Trace  GET root?sql=select+RowID%2CFirstName%2CLastName%2CYearOfBirth%2CYearOfDeath%2CSexe+from+People status=200 state=847 in=0 out=21078
16:47:15 SQL  select RowID,YearOfBirth,YearOfDeath from People
16:47:15 Trace  GET root?sql=select+RowID%2CYearOfBirth%2CYearOfDeath+from+People status=200 state=847 in=0 out=10694
16:47:15 SQL  select RowID,FirstName,LastName,YearOfBirth,YearOfDeath,Sexe from People where yearofbirth=:(1900):
16:47:15 Trace  GET root?sql=select+RowID%2CFirstName%2CLastName%2CYearOfBirth%2CYearOfDeath%2CSexe+from+People+where+yearofbirth%3D%3A%281900%29%3A status=200 state=847 in=0 out=107
16:47:15 Trace  DELETE root/People/16 status=200 state=848 in=0 out=0
16:47:15 DB  Delete People.ID=16
</code></pre>
<p>  这样，我们的日志视图工具可以作为远程日志服务器运行，并实时显示输入的事件。</p>
<p>  提供此类日志非常方便，尤其是在移动设备或远程计算机上调试应用程序时。</p>
<h3 id="toc_7">17.1.2. Smart Mobile Studio支持<a class="vnote-anchor" href="#toc_7" data-anchor-icon="#"></a></h3>
<p>  Smart Mobile Studio，请参阅http://www.smartmobilestudio.com，是一个完整的RAD环境，用于编写最先进的HTML5移动应用程序。它附带了一个完全成熟的编译器，能够将Object Pascal（称之为SmartPascal）编译成高度优化的原始JavaScript。</p>
<p>  有几种解决方案可以编译为JavaScript。</p>
<p>  事实上，我们可以找到几个编译器系列：</p>
<ul>
<li>JavaScript超集，添加可选的强类型和类，接近ECMAScript第六版：此类型的当前主要语言当然是TypeScript，由Anders Hejlsberg（Delphi语言和C#的创始人）设计，并由Microsoft发布；</li>
<li>新的语言，致力于使编写JavaScript程序更容易，使用替代语法和新概念（如classes、lambdas、scoping、splats、comprehensions...），这个家族中最相关的语言是CoffeeScript和Dart；</li>
<li>高级语言，如Google Web Toolkit（编译Java代码）、JSIL（通过mono的C#）或Smart Mobile Studio（来自object pascal）；</li>
<li>低级语言，如Emscripten（使用asm.js从LLVM字节码编译C/C ++）。</li>
</ul>
<p>  当然，从我们的观点来看，使用现代pascal对象是非常有趣的，因为它将利用我们自己的编码技能，并使我们能够在客户端和服务端共享代码。</p>
<h4 id="toc_8">17.1.2.1. 超越JavaScript<a class="vnote-anchor" href="#toc_8" data-anchor-icon="#"></a></h4>
<p>  Smart Pascal语言为JavaScript带来了强大的类型和真正的OOP，包括类、部分类、接口、继承，多态、虚拟和抽象类和方法、辅助、闭包、lambdas、枚举和集合、getter/setter表达式、运算符重载、契约编程。如果需要，您可以释放JavaScript的强大功能：变体类型用于支持动态类型，您可以用<code>asm .. end</code>块编写JavaScript代码。</p>
<p>  请参阅http://en.wikipedia.org/wiki/The_Smart_Pascal_programming_language</p>
<p>  生成的HTML5项目是自给自足的，无需外部JavaScript库，并且被编译为单个index.html文件（如果需要，还包括其css）。由编译器生成的JavaScript代码（由Eric Grange在Delphi中编写）具有非常高的质量，针对执行性能进行了最佳优化（在JIT或V8中），具有低内存消耗，并且可以被压缩、混合。</p>
<p>  <code>SmartCL</code>运行库将HTML5 API封装在一组纯粹的pascal类和函数中，并且可以使用集成表单设计器的IDE。您可以直接在IDE中调试应用程序（自2.1版开始，即使它还不稳定）或在浏览器中（IE、Chrome或有优秀的调试器FireBug），单步执行pascal对象代码（如果在<code>Project Options / Linker</code>中定义了<code>"Add source map (for debugging)"</code>）。</p>
<p>  使用PhoneGap等第三方工具，请参阅http://phonegap.com，您将能够为您的客户提供真正的原生iOS或Android应用程序，无需任何网络即可运行，并可访问任何现代智能手机的全部功能。应用程序大小将远小于Delphi FMX生成的应用程序（一个简单的Smart RESTful客户端，登录表单和ORM+SOA测试结果压缩为40 KB），并且可以在所有HTML5平台上无缝工作，包括大多数移动设备（如Windows Phone、Blackberry、Firefox OS或webOS）或桌面（Windows、Linux、BSD、MacOS）架构。</p>
<p>  因此，Smart Mobile Studio是实现富客户端AJAX或移动应用程序的理想平台，可与我们的客户端-服务端mORMot框架协同工作。</p>
<h4 id="toc_9">17.1.2.2. Smart Mobile Studio与mORMot配合使用<a class="vnote-anchor" href="#toc_9" data-anchor-icon="#"></a></h4>
<p>  不需要在Smart Mobile Studio IDE中安装软件包，客户端单元将直接从mORMot服务端生成。</p>
<p>  任意版本的Smart就足够了，请参阅http://smartmobilestudio.com/feature-matrix，您无需支付企业版就可以使用mORMot服务。当然，推荐使用专业版，因为基础版不支持IDE表单创建，这是AJAX应用程序的基础。</p>
<p>  与Smart专业版访问RemObjects或DataSnap服务的可用包装器相比，我们的mORMot客户端100％支持SmartPascal方言，无需将外部.js库链接到您的可执行文件，您将受益于Smart编译器的模糊处理和智能链接功能。</p>
<p>  唯一的要求是将mORMot跨平台单元复制到Smart Mobile Studio安装目录，可以通过三个复制指令来完成：</p>
<pre><code class="lang-console hljs">xcopy SynCrossPlatformSpecific.pas "c:\ProgramData\Optimale Systemer AS\Smart Mobile Studio\Libraries" /Y
xcopy SynCrossPlatformCrypto.pas "c:\ProgramData\Optimale Systemer AS\Smart Mobile Studio\Libraries" /Y
xcopy SynCrossPlatformREST.pas "c:\ProgramData\Optimale Systemer AS\Smart Mobile Studio\Libraries" /Y
</code></pre>
<p>  您可以在<code>CrossPlatform</code>和<code>SQLite3\Samples\29 - SmartMobileStudio Client\CopySynCrossPlatformUnits.bat</code>文件夹找到相应的批处理文件。</p>
<p>  事实上，Smart Mobile Studio不使用<code>SynCrossPlatformJSON.pas</code>单元：我们使用JavaScript的内置JSON序列化功能，使用变体动态类型，以及标准的<code>JSON.Stringify()</code>和<code>JSON.Parse()</code>函数。</p>
<h3 id="toc_10">17.1.3. 远程日志<a class="vnote-anchor" href="#toc_10" data-anchor-icon="#"></a></h3>
<p>  由于HTML5沙盒应用程序下没有真正的文件系统API，因此无法选择将日志记录到本地文件，即使与PhoneGap打包在一起，本地日志文件也不便于使用。</p>
<p>  日志生成将拥有与Delphi或FreePascal相同的方法和格式，请参阅<a href="">本地或远程日志记录</a>。</p>
<p>  <code>TSQLRest.Log(E: Exception)</code>方法也会跟踪异常的堆栈日志！我们的LogView工具 ，能够作为一个简单但高效的远程日志服务器和查看器运行，与框架的普通或跨平台单元共享。</p>
<p>  为Smart Mobile Studio客户端改进了专用的异步实现，以便收集多个事件并立即发送到远程服务器，以最大化带宽使用并让应用程序保持响应。</p>
<p>  它甚至可以在任何设备上轻松调试复杂的移动应用程序，甚至可以通过WiFi或3G/4G网络进行调试。技术支持可能会要求您的客户为特定案例启用日志记录，然后实时查看您的应用程序出了什么问题。</p>
<h2 id="toc_11">17.2. 生成客户端包装器<a class="vnote-anchor" href="#toc_11" data-anchor-icon="#"></a></h2>
<p>  即使手动编写客户端代码是可行的，您的mORMot服务端也能够通过专用的基于方法的服务和一组基于Mustache的模板创建客户端访问所需的源代码。</p>
<p>  <code>CrossPlatform\templates</code>文件夹中提供了以下模板：</p>
<table>
<thead>
<tr>
<th>单元名</th>
<th>编译目标</th>
</tr>
</thead>
<tbody>
<tr>
<td>CrossPlatform.pas.mustache</td>
<td>Delphi/FPC SynCrossPlatform*单元</td>
</tr>
<tr>
<td>Delphi.pas.mustache</td>
<td>Delphi Win32/Win64 mORMot单元</td>
</tr>
<tr>
<td>Delphi.pas.mustache</td>
<td>Smart Mobile Studio 2.1</td>
</tr>
</tbody>
</table>
<p>  将来，可能会添加其他包装器。你也可以编写自己的，可以包含在框架源代码中！我们热烈欢迎您的建议，特别是如果您想为Java或C＃客户端编写模板。生成的数据上下文已经包含与那些编译器相对应的数据类型：例如一个mORMot的RawUTF8字段或参数可以被识别为<code>"typeCS":"string"</code>、<code>"typeJava":"String"</code>、<code>"typeDelphi":"RawUTF8"</code>和<code>"typePascal":"string"</code>。</p>
<h3 id="toc_12">17.2.1. 发布代码生成器<a class="vnote-anchor" href="#toc_12" data-anchor-icon="#"></a></h3>
<p>  默认情况下，出于安全原因，代码生成未嵌入到mORMot RESTful服务器中。实际上，<code>mORMotWrapper.pas</code>单元将链接<code>mORMot.pas</code>和<code>SynMustache.pas</code>单元，并使用Mustache模板为给定的<code>TSQLRestServer</code>实例生成代码。</p>
<p>  我们将从<code>"SQLite3\Samples\14 - Interface based services"</code>文件夹中基于接口的服务示例代码开始，经过一些小的修改后，我们将服务器源代码复制到了<br>
<code>"SQLite3\Samples\27 - CrossPlatform Clients\Project14ServerHttpWrapper.dpr"</code>：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">program</span> Project14ServerHttpWrapper;

<span class="hljs-meta">{$APPTYPE CONSOLE}</span>

<span class="hljs-keyword">uses</span>
  SysUtils,
  Classes,
  SynCommons,
  mORMot,
  mORMotHttpServer,
  mORMotWrappers,
  Project14Interface <span class="hljs-keyword">in</span> <span class="hljs-string">'..\14 - Interface based services\Project14Interface.pas'</span>;

<span class="hljs-keyword">type</span>
  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TInterfacedObject, ICalculator)
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceCalculator</span>.<span class="hljs-title">Add</span><span class="hljs-params">(n1, n2: integer)</span>:</span> integer;
<span class="hljs-keyword">begin</span>
  result := n1+n2;
<span class="hljs-keyword">end</span>;

<span class="hljs-keyword">var</span>
  aModel: TSQLModel;
  aServer: TSQLRestServer;
  aHTTPServer: TSQLHttpServer;
<span class="hljs-keyword">begin</span>
  <span class="hljs-comment">// create a Data Model</span>
  aModel := TSQLModel.Create([],ROOT_NAME);
  <span class="hljs-keyword">try</span>
    <span class="hljs-comment">// initialize a TObjectList-based database engine</span>
    aServer := TSQLRestServerFullMemory.Create(aModel,<span class="hljs-string">'test.json'</span>,false,true);
    <span class="hljs-keyword">try</span>
      <span class="hljs-comment">// add the http://localhost:888/root/wrapper code generation web page</span>
      AddToServerWrapperMethod(aServer,
        [<span class="hljs-string">'..\..\..\CrossPlatform\templates'</span>,<span class="hljs-string">'..\..\..\..\CrossPlatform\templates'</span>]);
      <span class="hljs-comment">// register our ICalculator service on the server side</span>
      aServer.ServiceDefine(TServiceCalculator,[ICalculator],sicShared);
      <span class="hljs-comment">// launch the HTTP server</span>
      aHTTPServer := TSQLHttpServer.Create(PORT_NAME,[aServer],<span class="hljs-string">'+'</span>,useHttpApiRegisteringURI);
      <span class="hljs-keyword">try</span>
        aHTTPServer.AccessControlAllowOrigin := <span class="hljs-string">'*'</span>; <span class="hljs-comment">// for AJAX requests to work</span>
        writeln(<span class="hljs-string">#10</span><span class="hljs-string">'Background server is running.'</span>);
        writeln(<span class="hljs-string">'You can test http://localhost:'</span>,PORT_NAME,<span class="hljs-string">'/wrapper'</span>);
        writeln(<span class="hljs-string">#10</span><span class="hljs-string">'Press [Enter] to close the server.'</span><span class="hljs-string">#10</span>);
        readln;
      <span class="hljs-keyword">finally</span>
        aHTTPServer.Free;
      <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">finally</span>
      aServer.Free;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    aModel.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>.
</code></pre>
<p>  如您所见，我们刚刚添加了对mORMotWrappers单元的引用，以及对<code>AddToServerWrapperMethod()</code>的调用，以便发布可用的代码生成器。</p>
<p>  现在，如果您运行Project14ServerHttpWrapper服务器，并用您喜欢的浏览器指向<code>http://localhost:888/root/wrapper</code>，您将看到以下页面：</p>
<pre><code class="hljs">Client Wrappers

Available Templates:

* CrossPlatform
mORMotClient.pas - download as file - see as text - see template

* Delphi
mORMotClient.pas - download as file - see as text - see template

* SmartMobileStudio
mORMotClient.pas - download as file - see as text - see template
</code></pre>
<p>  您还可以获取相应的模板上下文。</p>
<p>  此处列出了指定文件夹中可用的每个<code>*.mustache</code>模板。上面的链接将允许下载客户端源代码单元，或在浏览器中将其显示为文本。模板也可以未渲染显示，以供参考。作为真实的Mustache模板，源代码文件是从数据上下文生成的，可以从<code>"template context"</code>链接以JSON的形式显示，调试自己的模板时，它可能会有所帮助。请注意，如果您修改并保存<code>.mustache</code>模板文件，只需在浏览器重新加载<code>"see as text"</code>页面，即可立即呈现您的修改（无需重新启动服务器）。</p>
<p>  生成的源代码将遵循模板名称，此处将始终下载为mORMotClient.pas。当然，您可以更改最终用户应用程序的单元名称。如果同一客户端同时访问多个mORMot服务器，修改是强制性的，这是面向服务的架构（SOA）项目中的情况。</p>
<p>  只需确保您永远不会手动更改mORMotClient.pas生成的内容。如有必要，您可以创建和自定义您自己的Mustache模板，以用于您的确切目标。通过设计，这种自动代码生成将需要在每次修改服务端ORM或SOA结构时重新创建客户端单元。实际上，正如<code>mORMotClient.pas</code>注释中所述，在重新生成后，此文件的任何手动修改都可能会丢失。</p>
<p>  要发布面向REST/ORM的程序的包装器，请查看<code>'28 - Simple RESTful ORM Server'</code>示例。</p>
<p>  如果您认为当前模板存在某些问题或需要一些增强功能，欢迎您在我们的论坛上向我们发送您的更改需求。一旦你使用了，您会发现Mustache模板可以很容易地使用。同样，如果您发现生成的数据上下文中缺少某些信息，如对于新的平台或语言，我们很乐意加强官方<code>mORMotWrapper.pas</code>流程。</p>
<h3 id="toc_13">17.2.2. Delphi/FreePascal客户端示例<a class="vnote-anchor" href="#toc_13" data-anchor-icon="#"></a></h3>
<p>  <code>"27 - CrossPlatform ClientsRegressionTests.dpr"</code>示例创建了一个mORMot服务器，它拥有自己的ORM数据模型，包含<code>TSQLRecordPeople</code>类和一组基于接口的SOA服务，其中一些包括复杂类型（如记录）。</p>
<p>  此示例使用生成的<code>mORMotClient.pas</code>，从上面的<code>CrossPlatform</code>模板的<code>"download as file"</code>链接获取。</p>
<p>  它的回归测试集（使用小型跨平台<code>TSynTest</code>单元测试类编写）将通过所有支持的身份验证方案对<code>PeopleServer</code>嵌入式实例执行远程ORM和SOA调用：</p>
<pre><code class="lang-console hljs"> Cross Platform Units for mORMot
---------------------------------

 1. Running "Iso8601DateTime"
    30003 tests passed in 00:00:018
 2. Running "Base64Encoding"
    304 tests passed in 00:00:000
 3. Running "JSON"
    18628 tests passed in 00:00:056
 4. Running "Model"
    1013 tests passed in 00:00:003
 5. Running "Cryptography"
    4 tests passed in 00:00:000

 Tests failed: 0 / 49952
 Time elapsed: 00:00:080

 Cross Platform Client for mORMot without authentication
---------------------------------------------------------

 1. Running "Connection"
    2 tests passed in 00:00:010
 2. Running "ORM"
    4549 tests passed in 00:00:160
 3. Running "ORMBatch"
    4564 tests passed in 00:00:097
 4. Running "Services"
    26253 tests passed in 00:00:302
 5. Running "CleanUp"
    1 tests passed in 00:00:000

 Tests failed: 0 / 35369
 Time elapsed: 00:00:574

 Cross Platform Client for mORMot using TSQLRestServerAuthenticationNone
-------------------------------------------------------------------------
  ...

 Cross Platform Client for mORMot using TSQLRestServerAuthenticationDefault
----------------------------------------------------------------------------
  ...
</code></pre>
<p>  生成的mORMotClient.pas单元用于上述所有“跨平台客户端”测试，涵盖框架的ORM和SOA功能。</p>
<h4 id="toc_14">17.2.2.1. 服务连接<a class="vnote-anchor" href="#toc_14" data-anchor-icon="#"></a></h4>
<p>  您可以手动连接到mORMot服务器：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> Model: TSQLModel;
    Client: TSQLRestClientHTTP;
...
  Model := TSQLModel.Create([TSQLAuthUser,TSQLAuthGroup,TSQLRecordPeople]);
  Client := TSQLRestClientHTTP.Create(<span class="hljs-string">'localhost'</span>,SERVER_PORT,Model);
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> Client.Connect <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Impossible to connect to the server'</span>);
  <span class="hljs-keyword">if</span> Client.ServerTimestamp=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Incorrect server'</span>);
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> Client.SetUser(TSQLRestAuthenticationDefault,<span class="hljs-string">'User'</span>,<span class="hljs-string">'synopse'</span>) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Impossible to authenticate to the server'</span>);
...
</code></pre>
<p>  或者您也可以使用<code>mORMotClient.pas</code>中生成的<code>GetClient()</code>函数：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-comment">/// create a TSQLRestClientHTTP instance and connect to the server</span>
<span class="hljs-comment">// - it will use by default port 888</span>
<span class="hljs-comment">// - secure connection will be established via TSQLRestServerAuthenticationDefault</span>
<span class="hljs-comment">// with the supplied credentials - on connection or authentication error,</span>
<span class="hljs-comment">// this function will raise a corresponding exception</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetClient</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aServerAddress, aUserName,aPassword: <span class="hljs-keyword">string</span>;
  aServerPort: integer=SERVER_PORT)</span>:</span> TSQLRestClientHTTP;
</code></pre>
<p>  可以这样使用：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> Client: TSQLRestClientHTTP;
...
  Client := GetClient(<span class="hljs-string">'localhost'</span>,<span class="hljs-string">'User'</span>,<span class="hljs-string">'synopse'</span>)<span class="hljs-string">'
</span></code></pre>
<p>  数据模型和需要的身份验证方案包含在<code>GetClient()</code>函数中，如果出现任何连接或身份验证问题，它将触发预期的<code>ERestException</code>。</p>
<h4 id="toc_15">17.2.2.2. CRUD/ORM远程访问<a class="vnote-anchor" href="#toc_15" data-anchor-icon="#"></a></h4>
<p>  借助<code>SynCrossPlatform*</code>单元，您可以使用常用的<code>TSQLRest</code> CRUD方法轻松地在mORMot服务端执行任何远程ORM操作。</p>
<p>  例如，<code>RegressionTests.dpr</code>示例执行以下操作：</p>
<pre><code class="lang-pascal hljs">  fClient.CallBackGet(<span class="hljs-string">'DropTable'</span>,[],Call,TSQLRecordPeople); <span class="hljs-comment">// call of method-based service</span>
  check(Call.OutStatus=HTTP_SUCCESS);
  people := TSQLRecordPeople.Create; <span class="hljs-comment">// create a record ORM</span>
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
      people.FirstName := <span class="hljs-string">'First'</span>+IntToStr(i);
      people.LastName := <span class="hljs-string">'Last'</span>+IntToStr(i);
      people.YearOfBirth := i+<span class="hljs-number">1800</span>;
      people.YearOfDeath := i+<span class="hljs-number">1825</span>;
      people.Sexe := TPeopleSexe(i <span class="hljs-keyword">and</span> <span class="hljs-number">1</span>);
      check(Client.Add(people,true)=i); <span class="hljs-comment">// add one record</span>
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    people.Free;
  <span class="hljs-keyword">end</span>;
...
  people := TSQLRecordPeople.CreateAndFillPrepare(fClient,<span class="hljs-string">''</span>,
    <span class="hljs-string">'yearofbirth=?'</span>,[<span class="hljs-number">1900</span>]); <span class="hljs-comment">// parameterized query returning one or several rows</span>
  <span class="hljs-keyword">try</span>
    n := <span class="hljs-number">0</span>;
    <span class="hljs-keyword">while</span> people.FillOne <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
      inc(n);
      check(people.ID=<span class="hljs-number">100</span>);
      check(people.FirstName=<span class="hljs-string">'First100'</span>);
      check(people.LastName=<span class="hljs-string">'Last100'</span>);
      check(people.YearOfBirth=<span class="hljs-number">1900</span>);
      check(people.YearOfDeath=<span class="hljs-number">1925</span>);
    <span class="hljs-keyword">end</span>;
    check(n=<span class="hljs-number">1</span>); <span class="hljs-comment">// we expected only one record here</span>
  <span class="hljs-keyword">finally</span>
    people.Free;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">if</span> i <span class="hljs-keyword">and</span> <span class="hljs-number">15</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
      fClient.Delete(TSQLRecordPeople,i) <span class="hljs-keyword">else</span> <span class="hljs-comment">// record deletion</span>
    <span class="hljs-keyword">if</span> i <span class="hljs-keyword">mod</span> <span class="hljs-number">82</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
      people := TSQLRecordPeople.Create;
      <span class="hljs-keyword">try</span>
        id := i+<span class="hljs-number">1</span>;
        people.ID := i;
        people.YearOfBirth := id+<span class="hljs-number">1800</span>;
        people.YearOfDeath := id+<span class="hljs-number">1825</span>;
        check(fClient.Update(people,<span class="hljs-string">'YEarOFBIRTH,YEarOfDeath'</span>)); <span class="hljs-comment">// record modification</span>
      <span class="hljs-keyword">finally</span>
        people.Free;
      <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    people := TSQLRecordPeople.Create(fClient,i); <span class="hljs-comment">// retrieve one instance from ID</span>
    <span class="hljs-keyword">try</span>
      <span class="hljs-keyword">if</span> i <span class="hljs-keyword">and</span> <span class="hljs-number">15</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span> <span class="hljs-comment">// was deleted</span>
        Check(people.ID=<span class="hljs-number">0</span>) <span class="hljs-keyword">else</span> <span class="hljs-keyword">begin</span>
        <span class="hljs-keyword">if</span> i <span class="hljs-keyword">mod</span> <span class="hljs-number">82</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
          id := i+<span class="hljs-number">1</span> <span class="hljs-keyword">else</span> <span class="hljs-comment">// was modified</span>
          id := i;
        Check(people.ID=i);
        Check(people.FirstName=<span class="hljs-string">'First'</span>+IntToStr(i));
        Check(people.LastName=<span class="hljs-string">'Last'</span>+IntToStr(i));
        Check(people.YearOfBirth=id+<span class="hljs-number">1800</span>);
        Check(people.YearOfDeath=id+<span class="hljs-number">1825</span>);
        Check(ord(people.Sexe)=i <span class="hljs-keyword">and</span> <span class="hljs-number">1</span>);
      <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">finally</span>
      people.Free;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  正如我们已经说过的那样，支持批次模式，使用经典的mORMot语法：</p>
<pre><code class="lang-pascal hljs">...
    res: TIntegerDynArray;
...
  fClient.BatchStart(TSQLRecordPeople);
  people := TSQLRecordPeople.Create;
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
      people.FirstName := <span class="hljs-string">'First'</span>+IntToStr(i);
      people.LastName := <span class="hljs-string">'Last'</span>+IntToStr(i);
      people.YearOfBirth := i+<span class="hljs-number">1800</span>;
      people.YearOfDeath := i+<span class="hljs-number">1825</span>;
      fClient.BatchAdd(people,true);
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    people.Free;
  <span class="hljs-keyword">end</span>;
  fClient.fBatchSend(res)=HTTP_SUCCESS);
  check(length(res)=<span class="hljs-number">200</span>);
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span>
    check(res[i-<span class="hljs-number">1</span>]=i); <span class="hljs-comment">// server returned the IDs of the newly created records</span>
</code></pre>
<p>  <code>TSQLRest</code>的<code>BatchAdd</code>/<code>BatchDelete</code>/<code>BatchUpdate</code>方法可以为客户端带来：</p>
<ul>
<li>更高的性能，特别是在多次插入或更新数据时；</li>
<li>事务支持：<code>TSQLRest.BatchStart()</code>有一个可选的<code>AutomaticTransactionPerRow</code>参数，默认设置为10000，这将在写入过程中创建服务端事务，在服务端启用数组绑定或SQL优化（如果可用），以及如果发生任何故障，将进行ACID回滚。</li>
</ul>
<p>  您可能注意到，上述所有代码都具有与标准mORMot客户端完全相同的结构和方法。</p>
<p>  生成的<code>mORMotClient.pas</code>单元包含所有必需的<code>TSQLRecord</code>类型及其使用的属性，包括枚举或复杂记录。该单元的唯一依赖是<code>SynCrossPlatform*</code>单元，因此将完全跨平台（而我们主要的<code>SynCommons.pas</code>和<code>mORMot.pas</code>单元仅针对Win32和Win64）。</p>
<p>  因此，您可以在Windows项目和任何支持的平台（甚至是AJAX）之间共享服务端和客户端代码（请参阅下面的“Smart Mobile Studio客户端示例”）。统一共享的代码库最终将减少实现和调试时间，这对于释放业务代码潜力和最大化收益至关重要。</p>
<h4 id="toc_16">17.2.2.3. 服务使用<a class="vnote-anchor" href="#toc_16" data-anchor-icon="#"></a></h4>
<p>  mORMot框架的最终目标是通过<a href="">面向服务的架构（SOA）</a>发布您的业务。</p>
<p>  因此，即使在Windows世界之外，也应该从任何类型的设备或平台提供这些服务。服务器能够生成客户端包装器代码，可以使用任何支持的身份验证方案使用任何<a href="">基于接口的客户端-服务端服务</a>。</p>
<p>  以下是为<code>RegressionTests.dpr</code>示例生成的<code>mORMotClient.pas</code>单元的摘录：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-comment">/// service implemented by TServiceCalculator</span>
  <span class="hljs-comment">// - you can access this service as such:</span>
  <span class="hljs-comment">// !var aCalculator: ICalculator;</span>
  <span class="hljs-comment">// !begin</span>
  <span class="hljs-comment">// !   aCalculator := TCalculator.Create(aClient);</span>
  <span class="hljs-comment">// !   // now you can use aCalculator methods</span>
  <span class="hljs-comment">// !...</span>
  ICalculator = <span class="hljs-keyword">interface</span>(IServiceAbstract)
    [<span class="hljs-string">'{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(<span class="hljs-keyword">const</span> n1: integer; <span class="hljs-keyword">const</span> n2: integer)</span>:</span> integer;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ToText</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: currency; <span class="hljs-keyword">const</span> Curr: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">var</span> Sexe: TPeopleSexe; <span class="hljs-keyword">var</span> <span class="hljs-keyword">Name</span>: <span class="hljs-keyword">string</span>)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RecordToText</span><span class="hljs-params">(<span class="hljs-keyword">var</span> Rec: TTestCustomJSONArraySimpleArray)</span>:</span> <span class="hljs-keyword">string</span>;
  <span class="hljs-keyword">end</span>;

  <span class="hljs-comment">/// implements ICalculator from http://localhost:888/root/Calculator</span>
  <span class="hljs-comment">// - this service will run in sicShared mode</span>
  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TServiceClientAbstract,ICalculator)
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span><span class="hljs-params">(aClient: TSQLRestClientURI)</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(<span class="hljs-keyword">const</span> n1: integer; <span class="hljs-keyword">const</span> n2: integer)</span>:</span> integer;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ToText</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: currency; <span class="hljs-keyword">const</span> Curr: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">var</span> Sexe: TPeopleSexe; <span class="hljs-keyword">var</span> <span class="hljs-keyword">Name</span>: <span class="hljs-keyword">string</span>)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RecordToText</span><span class="hljs-params">(<span class="hljs-keyword">var</span> Rec: TTestCustomJSONArraySimpleArray)</span>:</span> <span class="hljs-keyword">string</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  如您所见，已生成一个客户端自己的<code>ICalculator</code>专用类型，以使用服务端基于接口的<code>ICalculator</code>服务。</p>
<p>  它能够处理复杂类型，如枚举（如<code>TPeopleSexe</code>）和记录（如<code>TTestCustomJSONArraySimpleArray</code>），它们也定义在同一个<code>mORMotClient.pas</code>单元中。</p>
<p>  您可能注意到，<code>RawUTF8</code>类型已更改为标准的Delphi/FreePascal字符串类型，因为它是我们的<code>SynCrossPlatformJSON.pas</code>单元用于其所有JSON编码的本地类型。 当然，在最新版本的Delphi和FreePascal下，这些内容可能是Unicode编码的（对于UTF-16用于<code>string</code> = <code>UnicodeString</code>Delphi类型，对于UTF-8用于FreePascal / Lazarus字符串类型）。</p>
<p>  提供的回归测试显示了如何远程使用这些服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> calc: ICalculator;
    i,j: integer;
    sex: TPeopleSexe;
    <span class="hljs-keyword">name</span>: <span class="hljs-keyword">string</span>;
...
  calc := TServiceCalculator.Create(fClient);
  check(calc.InstanceImplementation=sicShared);
  check(calc.ServiceName=<span class="hljs-string">'Calculator'</span>);
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span>
    check(calc.Add(i,i+<span class="hljs-number">1</span>)=i*<span class="hljs-number">2</span>+<span class="hljs-number">1</span>);
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    sex := TPeopleSexe(i <span class="hljs-keyword">and</span> <span class="hljs-number">1</span>);
    <span class="hljs-keyword">name</span> := <span class="hljs-string">'Smith'</span>;
    calc.ToText(i,<span class="hljs-string">'$'</span>,sex,<span class="hljs-keyword">name</span>);
    check(sex=sFemale);
    check(<span class="hljs-keyword">name</span>=format(<span class="hljs-string">'$ %d for %s Smith'</span>,[i,SEX_TEXT[i <span class="hljs-keyword">and</span> <span class="hljs-number">1</span>]]));
  <span class="hljs-keyword">end</span>;
...
</code></pre>
<p>  与通常的mORMot客户端代码一样，创建了<code>TServiceCalculator</code>实例并将其分配给<code>ICalculator</code>局部变量。因此，无需使用<code>try ... finally Calc.Free end</code>语句块来避免任何内存泄漏：编译器将为<code>Calc: ICalculator</code>变量作用域创建这样一个隐藏块。</p>
<p>  使用<code>TServiceCalculator.Create</code>检索并检查<code>ICalculator</code>签名的服务端契约，如果它与<code>mORMotClient.pas</code>中标识的契约不匹配，则会引发<code>ERestException</code>。</p>
<p>  跨平台客户端能够管理服务实例的生命周期，尤其是<code>sicPerClient</code>模式。在这种情况下，将在服务端为每个客户端创建一个实现类实例，直到相应的接口实例被释放（即超出范围或赋值为<code>nil</code>），此时将释放服务端实例，就像使用常规mORMot客户端代码实例一样。</p>
<p>  注意，这里的所有过程都是同步执行的，即在阻塞模式下。您需要确保您的应用程序仍然能够响应，即使服务端执行了很多过程，因此响应可能延迟，在这种情况下，专用线程可能更有帮助。</p>
<h3 id="toc_17">17.2.3. Smart Mobile Studio客户端示例<a class="vnote-anchor" href="#toc_17" data-anchor-icon="#"></a></h3>
<p>  除了Delphi和FreePascal客户端，我们的框架还能够使用Smart Mobile Studio创建HTML5/AJAX富客户端访问任何mORMot服务端。</p>
<h4 id="toc_18">17.2.3.1. 在AJAX中添加两个数字<a class="vnote-anchor" href="#toc_18" data-anchor-icon="#"></a></h4>
<p>  您可以在<code>SQLite3\Samples\27 - CrossPlatform Clients\SmartMobileStudio</code>中找到使用基于接口的<code>TServiceCalculator.Add()</code>服务的简单客户端。</p>
<p>  如果<code>Project14ServerHttpWrapper</code>服务已经运行，您只需点击子文件夹中提供的<code>www\index.html</code>文件，然后，您将看到一个带有<code>"Server Connect"</code>按钮的网页，如果单击它，您将能够添加两个数字。这是一个完整的HTML5 Web应用程序，可以安全地连接到您的mORMot服务端，该服务可以运行在任何桌面浏览器（在Windows、Mac OS X或Linux）或任何移动设备（iPhone/iPad/Android/Windows 8 Mobile）。</p>
<p>  为了创建应用程序，我们只需在SmartMobileStudio的网页链接中单击<code>"download as file"</code>，然后将生成的文件复制到Smart Mobile新项目的源文件夹中。</p>
<p>  当然，我们还需将所需的<code>SynCrossPlatform*.pas</code>单元从mORMot源代码中复制到Smart库文件夹，如上所述，只需确保从最新版本的框架源代码中至少运行一次<code>CrossPlatform</code>文件夹中的<code>CopySynCrossPlatformUnits.bat</code>。</p>
<p>  之后，在窗体可视化编辑器上，我们添加了一个<code>BtnConnect</code>按钮，然后是一个<code>PanelCompute</code>面板，其中包含两个名为<code>EditA</code>和<code>EditB</code>的编辑字段，以及另外两个名为<code>BtnComputeAsynch</code>和<code>BtnComputeSynch</code>的按钮。 <code>LabelResult</code>标签将用于显示计算结果。 <code>BtnConnect</code>是一个切换按钮，用于显示<code>PanelCompute</code>面板，默认情况下隐藏该面板，具体取决于连接状态。</p>
<p><img src="./mORMot_Picture/SmartCalculator.png" alt="SmartCalculator.png" class="view-image"></p>
<p>  在<code>Form1.pas</code>单元源代码中，我们添加了对<code>SynCrossPlatformREST</code>和<code>mORMotClient</code>单元的引用，以及按钮的一些事件：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">unit</span> Form1;

<span class="hljs-keyword">interface</span>

<span class="hljs-keyword">uses</span>
  SmartCL.System, SmartCL.Graphics, SmartCL.Components, SmartCL.Forms,
  SmartCL.Fonts, SmartCL.Borders, SmartCL.Application, SmartCL.Controls.Panel,
  SmartCL.Controls.<span class="hljs-keyword">Label</span>, SmartCL.Controls.EditBox, SmartCL.Controls.Button,
  SynCrossPlatformREST, mORMotClient;

<span class="hljs-keyword">type</span>
  <span class="hljs-title">TForm1</span> = <span class="hljs-keyword">class</span>(TW3Form)
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">BtnComputeSynchClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">BtnComputeAsynchClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">BtnConnectClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
  <span class="hljs-keyword">private</span>
    <span class="hljs-meta">{$I 'Form1:intf'}</span>
  <span class="hljs-keyword">protected</span>
    Client: TSQLRestClientURI;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">InitializeForm</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">InitializeObject</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Resize</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>BtnConnect</code>事件将异步连接到服务器，使用<code>'User'</code>作为登录名，<code>'synopse'</code>作为密码（框架默认值）。</p>
<p>  我们使用我们生成的<code>mORMotClient.pas</code>单元发布的<code>GetClient()</code>函数：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-comment">/// create a TSQLRestClientHTTP instance and connect to the server</span>
<span class="hljs-comment">// - it will use by default port 888</span>
<span class="hljs-comment">// - secure connection will be established via TSQLRestServerAuthenticationDefault</span>
<span class="hljs-comment">// with the supplied credentials</span>
<span class="hljs-comment">// - request will be asynchronous, and trigger onSuccess or onError event</span>
<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">GetClient</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aServerAddress, aUserName,aPassword: <span class="hljs-keyword">string</span>;
  onSuccess, onError: TSQLRestEvent; aServerPort: integer=SERVER_PORT)</span>;</span>
</code></pre>
<p>  它使用两个回调，第一个是成功，第二个是失败时触发。 成功时，我们将使用刚刚创建的<code>TSQLRestClientURI</code>实例设置全局<code>Client</code>变量，然后显示两个字段和计算按钮：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TForm1</span>.<span class="hljs-title">BtnConnectClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> Client=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    GetClient(<span class="hljs-string">'127.0.0.1'</span>,<span class="hljs-string">'User'</span>,<span class="hljs-string">'synopse'</span>,
      lambda (aClient: TSQLRestClientURI)
        PanelCompute.Visible := true;
        W3Label1.Visible := true;
        W3Label2.Visible := true;
        LabelConnect.Caption := <span class="hljs-string">''</span>;
        BtnConnect.Caption := <span class="hljs-string">'Disconnect'</span>;
        LabelResult.Caption := <span class="hljs-string">''</span>;
        Client := aClient;
      <span class="hljs-keyword">end</span>,
      lambda
        ShowMessage(<span class="hljs-string">'Impossible to connect to the server!'</span>);
      <span class="hljs-keyword">end</span>)
  <span class="hljs-keyword">else</span> <span class="hljs-keyword">begin</span>
    PanelCompute.Visible := false;
    BtnConnect.Caption := <span class="hljs-string">'Server Connect'</span>;
    Client.Free;
    Client := <span class="hljs-keyword">nil</span>;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>GetClient()</code>函数需要两个回调，分别是<code>onSuccess</code>和<code>onError</code>，这里使用两个SmartPascal lambda块实现。</p>
<p>  现在我们连接到服务端，让我们做一些有用的计算！</p>
<p>  正如您在<code>mORMotClient.pas</code>生成的单元中所看到的，我们的基于接口的服务可以通过SmartPascal <code>TServiceCalculator</code>类（而不是接口）访问，每个方法有两个变体：一种异步方法，如<code>TServiceCalculator.Add()</code>，期望成功、错误回调，以及一个同步（阻塞）方法，如<code>TServiceCalculator._Add()</code>：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-comment">/// service accessible via http://localhost:888/root/Calculator</span>
  <span class="hljs-comment">// - this service will run in sicShared mode</span>
  <span class="hljs-comment">// - synchronous and asynchronous methods are available, depending on use case</span>
  <span class="hljs-comment">// - synchronous _*() methods will block the browser execution, so won't be</span>
  <span class="hljs-comment">// appropriate for long process - on error, they may raise EServiceException</span>
  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TServiceClientAbstract)
  <span class="hljs-keyword">public</span>
    <span class="hljs-comment">/// will initialize an access to the remote service</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span><span class="hljs-params">(aClient: TSQLRestClientURI)</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1: integer; n2: integer;
      onSuccess: <span class="hljs-keyword">procedure</span>(Result: integer)</span>;</span> onError: TSQLRestEvent);
    <span class="hljs-function"><span class="hljs-keyword">function</span> _<span class="hljs-title">Add</span><span class="hljs-params">(<span class="hljs-keyword">const</span> n1: integer; <span class="hljs-keyword">const</span> n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  因此，我们可以异步执行<code>Add()</code>服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TForm1</span>.<span class="hljs-title">BtnComputeAsynchClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
<span class="hljs-keyword">begin</span>
  TServiceCalculator.Create(Client).Add(
    StrToInt(EditA.Text),StrToInt(EditB.Text),
    lambda (res: integer)
      LabelResult.Caption := format(<span class="hljs-string">'Result = %d'</span>,[res]);
    <span class="hljs-keyword">end</span>,
    lambda
      ShowMessage(<span class="hljs-string">'Error calling the method!'</span>);
    <span class="hljs-keyword">end</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  或者同步执行<code>_Add()</code>服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TForm1</span>.<span class="hljs-title">BtnComputeSynchClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
<span class="hljs-keyword">begin</span>
  LabelResult.Caption := format(<span class="hljs-string">'Result = %d'</span>,
    [TServiceCalculator.Create(Client)._Add(
      StrToInt(EditA.Text),StrToInt(EditB.Text))]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  当然，同步代码更容易遵循和维护。公平地说，SmartPascal lambda语法不难读写。在浏览器调试器中，您可以在任何lambda块中轻松设置断点，并调试代码。</p>
<p>  请注意，如果服务器响应缓慢，您的整个Web应用程序将无响应，浏览器甚至可能会页面报错，建议终止其进程！</p>
<p>  因此，简单的服务可以以同步的方式编写，但是您严谨的业务代码应该使用异步回调，就像使用任何现代AJAX应用程序一样。</p>
<p>  由于其编译器的智能链接功能，只有已使用的单元版本将转换为JavaScript并包含在最终的<code>index.html</code>HTML5文件中。因此，拥有每种方法的同步和异步版本都不是问题。</p>
<h4 id="toc_19">17.2.3.2. CRUD / ORM远程访问<a class="vnote-anchor" href="#toc_19" data-anchor-icon="#"></a></h4>
<p>  如果服务器确实有一些ORM模型，则其TSQLRecord类也将成为<code>mORMotClient.pas</code>生成单元的一部分。所有类型，甚至是复杂的记录结构，都将按预期进行编码。</p>
<p>  例如，如果运行<code>RegressionTestsServer.dpr</code>服务器（在同一文件夹中），则可以从<code>http://localhost:888/root/wrapper</code>生成更完整的单元：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span> <span class="hljs-comment">// define some enumeration types, used below</span>
  TPeopleSexe = (sFemale, sMale);
  TRecordEnum = (reOne, reTwo, reLast);

<span class="hljs-keyword">type</span> <span class="hljs-comment">// define some record types, used as properties below</span>
  TTestCustomJSONArraySimpleArray = <span class="hljs-keyword">record</span>
    F: <span class="hljs-keyword">string</span>;
    G: <span class="hljs-keyword">array</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">string</span>;
    H: <span class="hljs-keyword">record</span>
      H1: integer;
      H2: <span class="hljs-keyword">string</span>;
      H3: <span class="hljs-keyword">record</span>
        H3a: boolean;
        H3b: TSQLRawBlob;
      <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">end</span>;
    I: TDateTime;
    J: <span class="hljs-keyword">array</span> <span class="hljs-keyword">of</span> <span class="hljs-keyword">record</span>
      J1: byte;
      J2: TGUID;
      J3: TRecordEnum;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">end</span>;

<span class="hljs-keyword">type</span>
  <span class="hljs-comment">/// service accessible via http://localhost:888/root/Calculator</span>
  <span class="hljs-comment">// - this service will run in sicShared mode</span>
  <span class="hljs-comment">// - synchronous and asynchronous methods are available, depending on use case</span>
  <span class="hljs-comment">// - synchronous _*() methods will block the browser execution, so won't be</span>
  <span class="hljs-comment">// appropriate for long process - on error, they may raise EServiceException</span>
  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TServiceClientAbstract)
  <span class="hljs-keyword">public</span>
    <span class="hljs-comment">/// will initialize an access to the remote service</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span><span class="hljs-params">(aClient: TSQLRestClientURI)</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1: integer; n2: integer;
      onSuccess: <span class="hljs-keyword">procedure</span>(Result: integer)</span>;</span> onError: TSQLRestEvent);
    <span class="hljs-function"><span class="hljs-keyword">function</span> _<span class="hljs-title">Add</span><span class="hljs-params">(<span class="hljs-keyword">const</span> n1: integer; <span class="hljs-keyword">const</span> n2: integer)</span>:</span> integer;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ToText</span><span class="hljs-params">(Value: currency; Curr: <span class="hljs-keyword">string</span>; Sexe: TPeopleSexe; <span class="hljs-keyword">Name</span>: <span class="hljs-keyword">string</span>;
      onSuccess: <span class="hljs-keyword">procedure</span>(Sexe: TPeopleSexe; <span class="hljs-keyword">Name</span>: <span class="hljs-keyword">string</span>)</span>;</span> onError: TSQLRestEvent);
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> _<span class="hljs-title">ToText</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: currency; <span class="hljs-keyword">const</span> Curr: RawUTF8; <span class="hljs-keyword">var</span> Sexe: TPeopleSexe; <span class="hljs-keyword">var</span> <span class="hljs-keyword">Name</span>: RawUTF8)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">RecordToText</span><span class="hljs-params">(Rec: TTestCustomJSONArraySimpleArray;
      onSuccess: <span class="hljs-keyword">procedure</span>(Rec: TTestCustomJSONArraySimpleArray; Result: <span class="hljs-keyword">string</span>)</span>;</span> onError: TSQLRestEvent);
    <span class="hljs-function"><span class="hljs-keyword">function</span> _<span class="hljs-title">RecordToText</span><span class="hljs-params">(<span class="hljs-keyword">var</span> Rec: TTestCustomJSONArraySimpleArray)</span>:</span> <span class="hljs-keyword">string</span>;
  <span class="hljs-keyword">end</span>;

  <span class="hljs-comment">/// map "People" table</span>
  <span class="hljs-title">TSQLRecordPeople</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">protected</span>
    fFirstName: <span class="hljs-keyword">string</span>;
    fLastName: <span class="hljs-keyword">string</span>;
    fData: TSQLRawBlob;
    fYearOfBirth: integer;
    fYearOfDeath: word;
    fSexe: TPeopleSexe;
    fSimple: TTestCustomJSONArraySimpleArray;
    <span class="hljs-comment">// those overriden methods will emulate the needed RTTI</span>
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ComputeRTTI</span>:</span> TRTTIPropInfos; <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">SetProperty</span><span class="hljs-params">(FieldIndex: integer; <span class="hljs-keyword">const</span> Value: variant)</span>;</span> <span class="hljs-keyword">override</span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetProperty</span><span class="hljs-params">(FieldIndex: integer)</span>:</span> variant; <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">public</span>
    <span class="hljs-keyword">property</span> FirstName: <span class="hljs-keyword">string</span> <span class="hljs-keyword">read</span> fFirstName <span class="hljs-keyword">write</span> fFirstName;
    <span class="hljs-keyword">property</span> LastName: <span class="hljs-keyword">string</span> <span class="hljs-keyword">read</span> fLastName <span class="hljs-keyword">write</span> fLastName;
    <span class="hljs-keyword">property</span> Data: TSQLRawBlob <span class="hljs-keyword">read</span> fData <span class="hljs-keyword">write</span> fData;
    <span class="hljs-keyword">property</span> YearOfBirth: integer <span class="hljs-keyword">read</span> fYearOfBirth <span class="hljs-keyword">write</span> fYearOfBirth;
    <span class="hljs-keyword">property</span> YearOfDeath: word <span class="hljs-keyword">read</span> fYearOfDeath <span class="hljs-keyword">write</span> fYearOfDeath;
    <span class="hljs-keyword">property</span> Sexe: TPeopleSexe <span class="hljs-keyword">read</span> fSexe <span class="hljs-keyword">write</span> fSexe;
    <span class="hljs-keyword">property</span> Simple: TTestCustomJSONArraySimpleArray <span class="hljs-keyword">read</span> fSimple <span class="hljs-keyword">write</span> fSimple;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  在上面的代码中，您可以看到<code>ICalculator</code>服务的几种方法，其中一些涉及复杂的<code>TTestCustomJSONArraySimpleArray</code>记录类型。 事实上，该单元的实现部分允许将这些记录序列化到JSON或从JSON序列化，即使使用模糊的JavaScript字段名称也是如此。</p>
<p>  一些枚举类型也被定义，因此可以帮助您的业务代码更具有表现力，这要归功于SmartPascal强类型。 与JavaScript本地弱类型和动态类型相比，这是一个巨大的改进。</p>
<p>  生成了一个<code>TSQLRecordPeople</code>类，它将映射以下Delphi类类型，如<code>PeopleServer.pas</code>单元所定义：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TSQLRecordPeople</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  <span class="hljs-keyword">protected</span>
    fData: TSQLRawBlob;
    fFirstName: RawUTF8;
    fLastName: RawUTF8;
    fYearOfBirth: integer;
    fYearOfDeath: word;
    fSexe: TPeopleSexe;
    fSimple: TTestCustomJSONArraySimpleArray;
  <span class="hljs-keyword">public</span>
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">InternalRegisterCustomProperties</span><span class="hljs-params">(Props: TSQLRecordProperties)</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> FirstName: RawUTF8 <span class="hljs-keyword">read</span> fFirstName <span class="hljs-keyword">write</span> fFirstName;
    <span class="hljs-keyword">property</span> LastName: RawUTF8 <span class="hljs-keyword">read</span> fLastName <span class="hljs-keyword">write</span> fLastName;
    <span class="hljs-keyword">property</span> Data: TSQLRawBlob <span class="hljs-keyword">read</span> fData <span class="hljs-keyword">write</span> fData;
    <span class="hljs-keyword">property</span> YearOfBirth: integer <span class="hljs-keyword">read</span> fYearOfBirth <span class="hljs-keyword">write</span> fYearOfBirth;
    <span class="hljs-keyword">property</span> YearOfDeath: word <span class="hljs-keyword">read</span> fYearOfDeath <span class="hljs-keyword">write</span> fYearOfDeath;
    <span class="hljs-keyword">property</span> Sexe: TPeopleSexe <span class="hljs-keyword">read</span> fSexe <span class="hljs-keyword">write</span> fSexe;
  <span class="hljs-keyword">public</span>
    <span class="hljs-keyword">property</span> Simple: TTestCustomJSONArraySimpleArray <span class="hljs-keyword">read</span> fSimple;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  这里，正如我们前面已经说过，手动的<code>InternalRegisterCustomProperties()</code>注册，发布了一个复杂的<code>TTestCustomJSONArraySimpleArray</code>记录字段。 由于SmartPascal在RTTI方面受到限制，因此代码生成器确实定义了一些<code>ComputeRTTI()</code>、<code>GetProperty()</code>、和<code>SetProperty()</code>受保护的方法，这些方法将在运行时执行所有属性的JSON编码。</p>
<p>  您可以看到原始Delphi <code>TSQLRecord</code>中的<code>RawUTF8</code>类型在转换生成为<code>mORMotClient.pas</code>单元时，按预期映射到标准SmartPascal字符串类型。</p>
<p>  然后，您的AJAX客户端可以通过标准CRUD操作轻松访问此<code>TSQLRecordPeople</code>内容。</p>
<p>  请参阅<code>SQLite3\Samples\29 - SmartMobileStudio Client</code>示例，例如以下行：</p>
<pre><code class="lang-pascal hljs">  people := new TSQLRecordPeople;
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">1</span> <span class="hljs-keyword">to</span> <span class="hljs-number">200</span> <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    assert(client.Retrieve(i,people));
    assert(people.ID=i);
    assert(people.FirstName=<span class="hljs-string">'First'</span>+IntToStr(i));
    assert(people.LastName=<span class="hljs-string">'Last'</span>+IntToStr(i));
    assert(people.YearOfBirth=id+<span class="hljs-number">1800</span>);
    assert(people.YearOfDeath=id+<span class="hljs-number">1825</span>);
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  这里，客户端变量是一个<code>TSQLRestClientURI</code>实例，由<code>mORMotClient.pas</code>中生成的<code>GetClient()</code> <code>onSuccess</code>回调返回。</p>
<p>  您有可用的<code>Add()</code>、<code>Delete()</code>、<code>Update()</code>、<code>FillPrepare()</code>、<code>CreateAndFillPrepare()</code>和<code>Batch*()</code>方法，可以安全地从您的AJAX客户端访问您的数据。</p>
<p>  如果在服务端更新了数据模型，只需从<code>http://localhost:888/root/wrapper</code>重新生成<code>mORMotClient.pas</code>单元，然后重建Smart Mobile Studio项目以反映对ORM数据模型所做的所有更改， 或您的SOA可用服务。</p>
<p>  由于SmartPascal强类型，服务器期望的任何重大变化将立即在编译时报告，而不是在运行时报告，就像常规JavaScript客户端一样。</p>

    </div>
</div>
</div>

<div id="container-floating" style="display:none;" class="d-none d-md-block d-xl-block">
    <div id="floating-button" onclick="toggleMore()">
        <p id="floating-more" class="more">&gt;</p>
    </div>
</div>

<!--
<div class="footer" id="vnote-footer">
    <p>Generated by <em><a href="https://tamlok.github.io/vnote/">VNote</a></em>.</p>
</div>
-->
</body>
</html>
